// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

package org.cef;

import com.jetbrains.cef.JCefAppConfig;
import com.jetbrains.cef.remote.CefServer;
import com.jetbrains.cef.remote.NativeServerManager;
import com.jetbrains.cef.remote.ThriftTransport;
import com.jetbrains.cef.remote.browser.RemoteClient;
import com.jetbrains.cef.remote.callback.RemoteSchemeHandlerFactory;
import org.cef.browser.CefRendering;
import org.cef.callback.CefSchemeHandlerFactory;
import org.cef.handler.CefAppHandler;
import org.cef.handler.CefAppHandlerAdapter;
import org.cef.handler.CefAppStateHandler;
import org.cef.misc.CefLog;
import org.cef.misc.Utils;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

/**
 * Exposes static methods for managing the global CEF context.
 */
public class CefApp extends CefAppHandlerAdapter {
    public class CefVersion {
        public final int JCEF_COMMIT_NUMBER;

        public final String JCEF_COMMIT_HASH;

        public final int CEF_VERSION_MAJOR;
        public final int CEF_VERSION_MINOR;
        public final int CEF_VERSION_PATCH;
        public final int CEF_COMMIT_NUMBER;

        public final int CHROME_VERSION_MAJOR;
        public final int CHROME_VERSION_MINOR;
        public final int CHROME_VERSION_BUILD;
        public final int CHROME_VERSION_PATCH;

        protected CefVersion(int jcefCommitNo, String jcefCommitHash, int cefMajor, int cefMinor, int cefPatch,
                           int cefCommitNo, int chrMajor, int chrMin, int chrBuild, int chrPatch) {
            JCEF_COMMIT_NUMBER = jcefCommitNo;
            JCEF_COMMIT_HASH = jcefCommitHash;

            CEF_VERSION_MAJOR = cefMajor;
            CEF_VERSION_MINOR = cefMinor;
            CEF_VERSION_PATCH = cefPatch;
            CEF_COMMIT_NUMBER = cefCommitNo;

            CHROME_VERSION_MAJOR = chrMajor;
            CHROME_VERSION_MINOR = chrMin;
            CHROME_VERSION_BUILD = chrBuild;
            CHROME_VERSION_PATCH = chrPatch;
        }

        public String getJcefVersion() {
            return CEF_VERSION_MAJOR + "." + CEF_VERSION_MINOR + "." + CEF_VERSION_PATCH + "."
                    + JCEF_COMMIT_NUMBER + "." + JCEF_COMMIT_HASH;
        }

        public String getCefVersion() {
            return CEF_VERSION_MAJOR + "." + CEF_VERSION_MINOR + "." + CEF_VERSION_PATCH;
        }

        public String getChromeVersion() {
            return CHROME_VERSION_MAJOR + "." + CHROME_VERSION_MINOR + "." + CHROME_VERSION_BUILD
                    + "." + CHROME_VERSION_PATCH;
        }

        @Override
        public String toString() {
            return "JCEF Version = " + getJcefVersion() + "\n"
                    + "CEF Version = " + getCefVersion() + "\n"
                    + "Chromium Version = " + getChromeVersion();
        }
    }

    /**
     * The CefAppState gives you a hint if the CefApp is already usable or not
     * usable any more. See values for details.
     */
    public enum CefAppState {
        /**
         * No CefApp instance was created yet. Call getInstance() to create a new
         * one.
         */
        NONE,

        /**
         * CefApp is new created but not initialized yet. No CefClient and no
         * CefBrowser was created until now.
         */
        NEW,

        /**
         * CefApp is in its initializing process. Please wait until initializing is
         * finished.
         */
        INITIALIZING,

        /**
         * CefApp is up and running. At least one CefClient was created and the
         * message loop is running. You can use all classes and methods of JCEF now.
         */
        INITIALIZED,

        /**
         * CefApp is in its shutdown process. All CefClients and CefBrowser
         * instances will be disposed. No new CefClient or CefBrowser is allowed to
         * be created. The message loop will be performed until all CefClients and
         * all CefBrowsers are disposed completely.
         */
        SHUTTING_DOWN,

        /**
         * CefApp is terminated and can't be used any more. You can shutdown the
         * application safely now.
         */
        TERMINATED
    }

    /**
     * According to the singleton pattern, this attribute keeps
     * one single object of this class.
     */
    private static CefApp self = null;
    private static CefAppHandler userAppHandler_ = null;
    private static final CompletableFuture<Void> ourStartupFeature = new CompletableFuture<>();
    private final CefAppHandler appHandler_;
    private CefAppState state_ = CefAppState.NONE;
    private Timer workTimer_ = null;
    private final HashSet<CefClient> clients_ = new HashSet<CefClient>();
    private CefSettings settings_ = null;
    private final CefServer server_;

    //
    // Background initialization support
    //
    private volatile boolean isInitialized_ = false;
    private final LinkedList<CefAppStateHandler> initializationListeners_ = new LinkedList<>();

    // Constants for testing JBR-5530
    private static final boolean PREINIT_ON_ANY_THREAD = Utils.getBoolean("jcef_app_preinit_any");
    private static final int PREINIT_TEST_DELAY_MS = Utils.getInteger("jcef_app_preinit_test_delay_ms", 0);
    private static final int INIT_TEST_DELAY_MS = Utils.getInteger("jcef_app_init_test_delay_ms", 0);

    // Support for JBR-4430
    private static final boolean IS_REMOTE_ENABLED = isRemoteSupported() ? Boolean.getBoolean("jcef.remote.enabled") : false;

    /**
     * To get an instance of this class, use the method
     * getInstance() instead of this CTOR.
     * <p>
     * The CTOR is called by getInstance() as needed and
     * loads all required JCEF libraries.
     */
    private CefApp(String[] args, CefSettings settings, CefServer server) {
        super(args);
        appHandler_ = userAppHandler_;
        userAppHandler_ = null;
        if (settings != null) settings_ = settings.clone();
        setState(CefAppState.NEW);
        server_ = server;

        if (server_ != null) {
            server_.setCefApp(this);

            // Perform CefServer initialization in separate thread.
            new Thread(()->{
                if (server_.start(appHandler_)) {
                    CefLog.Debug("%s: native CefServer is initialized.", this);
                    setState(CefAppState.INITIALIZED);
                    synchronized (initializationListeners_) {
                        isInitialized_ = true;
                        initializationListeners_.forEach(l -> l.stateHasChanged(CefAppState.INITIALIZED));
                        initializationListeners_.clear();
                    }
                    CefLog.Info("%s: connected to CefServer. JCEF version: %s", this, getVersion());
                }
            }, "CefInitialize-thread").start();
            return;
        }

        ourStartupFeature.thenRunAsync(() -> {
            // Perform native pre-initialization.
            // This code will save global pointer to JVM instance.
            // Execute on the AWT event dispatching thread to store JNI context from EDT
            // NOTE: in practice it seems that this method can be called from any thread (at tests
            // execute successfully)
            // TODO: ensure and make all initialization steps in single bg thread.
            preinit(args);
            initialize();
        },
        new NamedThreadExecutor("CefInitialize-thread"));
    }

    private void preinit(String[] args) throws RuntimeException {
        // Start preInit
        AtomicBoolean success = new AtomicBoolean(false);
        if (PREINIT_ON_ANY_THREAD)
            success.set(N_PreInitialize());
        else {
            try {
                SwingUtilities.invokeAndWait(() -> success.set(N_PreInitialize()));
            } catch (InterruptedException | InvocationTargetException e) {
                throw new RuntimeException("Failed to do JCEF preinit", e);
            }
        }
        if (!success.get()) {
            throw new RuntimeException("Failed to do JCEF preinit");
        }
    }

    // Notifies (in initialization thread) listener that native context has been initialized.
    // When context is already initialized then listener executes immediately.
    public void onInitialization(CefAppStateHandler initListener) {
        onInitialization(initListener, false);
    }

    public void onInitialization(CefAppStateHandler initListener, boolean first) {
        synchronized (initializationListeners_) {
            if (isInitialized_)
                initListener.stateHasChanged(CefAppState.INITIALIZED);
            else {
                if (first)
                    initializationListeners_.addFirst(initListener);
                else
                    initializationListeners_.addLast(initListener);
            }
        }
    }

    /**
     * Assign an AppHandler to CefApp. The AppHandler can be used to evaluate
     * application arguments, to register your own schemes and to hook into the
     * shutdown sequence. See CefAppHandler for more details.
     * <p>
     * This method must be called before CefApp is initialized. CefApp will be
     * initialized automatically if you call createClient() the first time.
     *
     * @param appHandler An instance of CefAppHandler.
     * @throws IllegalStateException in case of CefApp is already initialized
     */
    public static void addAppHandler(CefAppHandler appHandler) throws IllegalStateException {
        if (self != null && !IS_REMOTE_ENABLED)
            throw new IllegalStateException("Must be called before CefApp is initialized");
        userAppHandler_ = appHandler;
    }

    public static void setDefaultRenderingFactory(Supplier<CefRendering> factory) {
        RemoteClient.setDefaultRenderingFactory(factory);
    }

    /**
     * Get an instance of this class.
     *
     * @return an instance of this class
     */
    public static synchronized CefApp getInstance() {
        if (IS_REMOTE_ENABLED) {
            // Return default instance if exists.
            if (self != null)
                return self;
        }
        return getInstance(null, null);
    }

    public static synchronized CefApp getInstance(String[] args) {
        return getInstance(args, null);
    }

    public static synchronized CefApp getInstance(CefSettings settings) {
        return getInstance(null, settings);
    }

    public static synchronized CefApp getInstance(String[] args, CefSettings settings) {
        if (IS_REMOTE_ENABLED) {
            // 1. Get command line args (from passed arguments and userAppHandler_)
            final String[] realArgs;
            if (args != null && args.length > 0)
                realArgs = args;
            else if (userAppHandler_ instanceof CefAppHandlerAdapter) {
                realArgs = ((CefAppHandlerAdapter)userAppHandler_).getArgs();
            } else {
                if (userAppHandler_ != null)
                    CefLog.Error("Unsupported class of CefAppHandler %s. Overridden command-line arguments will be ignored.", CefAppHandler.class);
                realArgs = null;
            }

            // 2. Try to find the existing instance with the same args and settings.
            CefServer s = CefServer.findRunningInstance(realArgs, settings);
            if (s != null) {
                CefLog.Debug("Found running CefServer instance %s for settings: %s", s.toStringDetailed(), settings.getDescription());
                return s.getCefApp();
            }

            // 3. Create new CefApp instance.
            ThriftTransport st = ThriftTransport.ourDefaultServer;
            ThriftTransport ct = ThriftTransport.ourDefaultClient;
            if (CefServer.getInstancesCount() > 0) {
                if (ThriftTransport.isTcpUsed()) {
                    // change tcp transport with use of new free port
                    ct = new ThriftTransport(ThriftTransport.findFreePort(null));
                    Set<Integer> exclude = new HashSet<>(); exclude.add(ct.getPort());
                    st = new ThriftTransport(ThriftTransport.findFreePort(exclude));
                } else {
                    // Change pipe transport with use of suffix.
                    final String suffix = "" + System.currentTimeMillis();
                    ct = new ThriftTransport(ThriftTransport.getJavaHandlersPipe(suffix));
                    st = new ThriftTransport(ThriftTransport.getServerPipe(suffix));
                }
            }
            s = new CefServer(st, ct, realArgs, settings);
            CefApp result = new CefApp(realArgs, settings, s);

            // 4. Set default instance
            if (self == null)
                self = result;

            return result;
        }

        if (settings != null) {
            if (self != null)
                throw new IllegalStateException("Settings can only be passed to CEF"
                        + " before createClient is called the first time. Current state is " + self.state_);
        }
        if (self == null)
            self = new CefApp(args, settings, null);
        else if (self.isTerminated())
            throw new IllegalStateException("CefApp was terminated");

        return self;
    }

    public static synchronized CefApp findRunningInstance(String[] args, CefSettings settings) {
        if (!IS_REMOTE_ENABLED)
            return null;

        final CefServer s = CefServer.findRunningInstance(args, settings);
        return s != null ? s.getCefApp() : null;
    }

    public static synchronized CefApp getInstanceIfAny() {
        return self;
    }

    public static synchronized void setDefaultInstance(CefApp cefApp) {
        self = cefApp;
    }

    public final synchronized void setSettings(CefSettings settings) throws IllegalStateException {
        if (state_.compareTo(CefAppState.NEW) > 0)
            throw new IllegalStateException("Settings can only be passed to CEF"
                    + " before createClient is called the first time. Current state is " + state_);
        settings_ = settings.clone();
    }

    public void setDisconnectionCallback(Runnable disconnectionCallback) {
        if (server_ != null)
            server_.setDisconnectionCallback(disconnectionCallback);
        else
            CefLog.Error("setDisconnectionCallback is not supported for in-process CEF instance");
    }

    public final CefVersion getVersion() {
        if (server_ != null) {
            // TODO: request remaining params from server
            return new CefVersion(0, "0", 0, 0, 0, 0, 0, 0, 0, 0) {
                @Override
                public String toString() {
                    return "remote_" + server_.getVersion();
                }
                @Override
                public String getJcefVersion() {
                    return "remote_" + server_.getVersion();
                }
            };
        }
        try {
            return N_GetVersion();
        } catch (UnsatisfiedLinkError ule) {
            CefLog.Error("Failed to get CEF version. %s", ule.getMessage());
        }
        return null;
    }

    public static final boolean isRemoteSupported() {
        return NativeServerManager.isRemoteSupported();
    }

    public static final boolean isRemoteEnabled() { return IS_REMOTE_ENABLED; }

    public final CefServer getServer() { return server_; }

    @Override
    public String toString() {
        if (server_ == null)
            return "CefApp";
        return "CefApp(" + server_.toStringShort() + ")";
    }

    /**
     * Returns the current state of CefApp.
     *
     * @return current state.
     */
    public static CefAppState getState() {
        final CefApp app = self;
        return app == null ? CefAppState.NONE : app.state_;
    }

    public synchronized boolean isTerminated() { return state_ == CefAppState.TERMINATED; }
    public synchronized boolean isShuttingDown() { return state_ == CefAppState.TERMINATED || state_ == CefAppState.SHUTTING_DOWN; }

    private synchronized void setState(final CefAppState state) {
        if (state.compareTo(state_) < 0) {
            String errMsg = "" + this + ": state cannot go backward. Current state " + state_ + ". Proposed state " + state;
            CefLog.Error(errMsg);
            throw new IllegalStateException(errMsg);
        }
        CefLog.Info("%s: set state %s", this, state);
        state_ = state;
        // Execute on the AWT event dispatching thread.
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                CefAppHandler handler = appHandler_ == null ? CefApp.this : appHandler_;
                if (handler != null) handler.stateHasChanged(state);
            }
        });
    }

    /**
     * To shutdown the system, it's important to call the dispose
     * method. Calling this method closes all client instances with
     * and all browser instances each client owns. After that the
     * message loop is terminated and CEF is shutdown.
     */
    public synchronized final void dispose() {
        switch (state_) {
            case NEW:
                // Nothing to do inspite of invalidating the state
                setState(CefAppState.TERMINATED);
                break;

            case INITIALIZING:
            case INITIALIZED:
                // (3) Shutdown sequence. Close all clients and continue.
                setState(CefAppState.SHUTTING_DOWN);
                if (clients_.isEmpty()) {
                    finishShutdown();
                } else {
                    // shutdown() will be called from clientWasDisposed() when the last
                    // client is gone.
                    // Use a copy of the HashSet to avoid iterating during modification.
                    CefLog.Debug("%s: dispose clients before shutting down", this);
                    HashSet<CefClient> clients = new HashSet<CefClient>(clients_);
                    for (CefClient c : clients) {
                        CefLog.Debug("%s: dispose client %s", this, c);
                        c.dispose();
                    }
                }

                break;

            case NONE:
            case SHUTTING_DOWN:
            case TERMINATED:
                // Ignore shutdown, CefApp is already terminated, in shutdown progress
                // or was never created (shouldn't be possible)
                break;
        }
    }

    /**
     * Creates a new client instance and returns it to the caller.
     * One client instance is responsible for one to many browser
     * instances
     *
     * @return a new client instance
     */
    public synchronized CefClient createClient() {
        if (state_.compareTo(CefAppState.SHUTTING_DOWN) >= 0) {
            String errMsg = "" + this + ": can't create client in state " + state_;
            CefLog.Error(errMsg);
            throw new IllegalStateException(errMsg);
        }

        CefClient client = server_ != null ? new CefClient(server_.createClient()) : new CefClient();
        onInitialization(client, true);
        clients_.add(client);
        return client;
    }

    /**
     * Register a scheme handler factory for the specified |scheme_name| and
     * optional |domain_name|. An empty |domain_name| value for a standard scheme
     * will cause the factory to match all domain names. The |domain_name| value
     * will be ignored for non-standard schemes. If |scheme_name| is a built-in
     * scheme and no handler is returned by |factory| then the built-in scheme
     * handler factory will be called. If |scheme_name| is a custom scheme then
     * also implement the CefApp::OnRegisterCustomSchemes() method in all
     * processes. This function may be called multiple times to change or remove
     * the factory that matches the specified |scheme_name| and optional
     * |domain_name|. Returns false if an error occurs. This function may be
     * called on any thread in the browser process.
     */
    public boolean registerSchemeHandlerFactory(String schemeName, String domainName, CefSchemeHandlerFactory factory) {
        if (server_ != null) {
            server_.onConnected(()->{
                RemoteSchemeHandlerFactory rf = RemoteSchemeHandlerFactory.create(factory);
                server_.exec(s -> s.SchemeHandlerFactory_Register(schemeName, domainName, rf.thriftId()));
            }, "registerSchemeHandlerFactory", true);
            return true;
        }

        onInitialization(state -> {
            if (!N_RegisterSchemeHandlerFactory(schemeName, domainName, factory))
                CefLog.Error("Can't register scheme [%s:%s]", schemeName, domainName);
        });
        return true;
    }

    /**
     * Clear all registered scheme handler factories. Returns false on error. This
     * function may be called on any thread in the browser process.
     */
    public boolean clearSchemeHandlerFactories() {
        if (server_ != null) {
            server_.onConnected(()->{
                server_.exec(s -> s.ClearAllSchemeHandlerFactories());
            }, "clearSchemeHandlerFactories", false);
            return true;
        }

        if (!isInitialized_)
            return false;
        return N_ClearSchemeHandlerFactories();
    }

    /**
     * This method is called by a CefClient if it was disposed. This causes
     * CefApp to clean up its list of available client instances. If all clients
     * are disposed, CefApp will be shutdown.
     *
     * @param client the disposed client.
     */
    protected final synchronized void clientWasDisposed(CefClient client) {
        clients_.remove(client);
        synchronized (initializationListeners_) {
            initializationListeners_.remove(client);
        }
        CefLog.Debug("%s: client was disposed: %s [clients count %d]", this, client, clients_.size());
        if (clients_.isEmpty() && state_.compareTo(CefAppState.SHUTTING_DOWN) >= 0) {
            // Shutdown native system.
            finishShutdown();
        }
    }

    private static void testSleep(int ms) {
        if (ms > 0) {
            CefLog.Debug("testSleep %s ms", ms);
            try {
                Thread.sleep(ms);
            } catch (InterruptedException ignored) {
            }
        }
    }

    /**
     * Initialize the context. Can be executed in any thread.
     */
    private void initialize() {
        setState(CefAppState.INITIALIZING);
        testSleep(INIT_TEST_DELAY_MS);

        String logMsg = "Initialize CefApp on " + Thread.currentThread();
        if (settings_.log_severity == CefSettings.LogSeverity.LOGSEVERITY_INFO || settings_.log_severity == CefSettings.LogSeverity.LOGSEVERITY_VERBOSE)
            CefLog.Info(logMsg);
        else
            CefLog.Debug(logMsg);

        CefSettings settings = settings_ != null ? settings_ : new CefSettings();

        boolean success = N_Initialize(appHandler_ == null ? this : appHandler_, settings, false);
        if (success) {
            CefLog.Debug("CefApp: native initialization is finished.");
            setState(CefAppState.INITIALIZED);
            synchronized (initializationListeners_) {
                isInitialized_ = true;
                initializationListeners_.forEach(l -> l.stateHasChanged(CefAppState.INITIALIZED));
                initializationListeners_.clear();
            }
            CefLog.Info("version: %s | settings: %s", getVersion(), settings.getDescription());
        } else
            CefLog.Error("CefApp: N_Initialize failed.");
    }

    /**
     * Shut down the context.
     */
    private void finishShutdown() {
        if (server_ != null) {
            server_.stop();
            setState(CefAppState.TERMINATED);
            return;
        }
        new Thread(() -> {
            // Empiric observation: to avoid crashes we must perform native shutdown in next manner:
            // last client closed -> small pause (to allow CEF finish some bg tasks) -> call CefShutdown
            // This small pause will be in bg thread, see JBR-5822.
            final int sleepBeforeShutdown = Utils.getInteger("JCEF_SLEEP_BEFORE_SHUTDOWN", 999);
            if (sleepBeforeShutdown > 0) {
                try {
                    CefLog.Debug("Sleep before native shutdown for %d ms, thread %s.", sleepBeforeShutdown, Thread.currentThread());
                    Thread.sleep(sleepBeforeShutdown);
                } catch (InterruptedException e) {}
            }

            // Shutdown native CEF.
            CefLog.Info("Perform native shutdown of CEF on thread %s.", Thread.currentThread());
            N_Shutdown();

            setState(CefAppState.TERMINATED);
        }, "CEF-shutdown-thread").start();
    }

    /**
     * Perform a single message loop iteration. Used on all platforms except
     * Windows with windowed rendering.
     */
    public final void doMessageLoopWork(final long delay_ms) {
        if (server_ != null) {
            CefLog.Error("doMessageLoopWork musn't be called in remote mode.");
            return;
        }
        // Execute on the AWT event dispatching thread.
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                if (isTerminated()) return;

                // The maximum number of milliseconds we're willing to wait between
                // calls to DoMessageLoopWork().
                final long kMaxTimerDelay = 1000 / 30; // 30fps

                if (workTimer_ != null) {
                    workTimer_.stop();
                    workTimer_ = null;
                }

                if (delay_ms <= 0) {
                    // Execute the work immediately.
                    N_DoMessageLoopWork();

                    // Schedule more work later.
                    doMessageLoopWork(kMaxTimerDelay);
                } else {
                    long timer_delay_ms = delay_ms;
                    // Never wait longer than the maximum allowed time.
                    if (timer_delay_ms > kMaxTimerDelay) timer_delay_ms = kMaxTimerDelay;

                    workTimer_ = new Timer((int) timer_delay_ms, new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent evt) {
                            // Timer has timed out.
                            workTimer_.stop();
                            workTimer_ = null;

                            N_DoMessageLoopWork();

                            // Schedule more work later.
                            doMessageLoopWork(kMaxTimerDelay);
                        }
                    });
                    workTimer_.start();
                }
            }
        });
    }

    /**
     * This method must be called at the beginning of the main() method to perform platform-specific startup
     * initialization. On Linux this initializes Xlib multithreading and on macOS this dynamically loads the
     * CEF framework. Can be executed in any thread.
     *
     * @param args Command-line arguments were used only to get CEF framework path in OSX.
     */
    public static boolean startup(String[] args) {
        if (IS_REMOTE_ENABLED)
            return true;

        if (OS.isMacintosh() && args.length == 0) {
            // this condition is to be removed after adapting IJ
            startupAsync(JCefAppConfig.getJbrFrameworkPathOSX());
            return true;
        }

        String frameworkPathOSX = null;
        String switchPrefix = "--framework-dir-path=";
        for (String arg : args) {
            if (arg.startsWith(switchPrefix)) {
                frameworkPathOSX = new File(arg.substring(switchPrefix.length())).getAbsolutePath();
            }
        }
        startupAsync(frameworkPathOSX);
        return true;
    }

    /**
     * @deprecated Use @{@link CefApp#startupAsync(String)}. To be removed in 242
     */
    @Deprecated(forRemoval = true)
    public static void startupAsync() {
        startupAsync(JCefAppConfig.getJbrFrameworkPathOSX());
    }

    /**
     * Asynchronously starts up the CEF framework.
     * <p>
     * This method starts up the CEF framework on a separate thread, allowing the application to continue
     * executing while the framework is being initialized. Once the framework is initialized, the
     * operation is considered complete and any registered completion handlers are invoked.
     * <p>
     * This method must be called at the beginning of the main() method to perform platform-specific startup
     * initialization. On Linux this initializes Xlib multithreading and on macOS this dynamically loads the
     * CEF framework. Can be executed in any thread.
     * <p>
     * If the remote debugging feature is enabled, the method returns without actually starting up the
     * framework. However, the method must be called anyway to unblock further initialization.
     *
     * @param frameworkPath The path to the CEF framework on the system.
     *                      Used only on macOS.
     */
    public static void startupAsync(String frameworkPath) {
        if (IS_REMOTE_ENABLED)
            return;

        new NamedThreadExecutor("CefStartup-thread").execute(()->{
            try {
                if (OS.isWindows()) {
                    // [tav] "jawt" is loaded by JDK AccessBridgeLoader that leads to UnsatisfiedLinkError
                    try {
                        SystemBootstrap.loadLibrary("jawt");
                    } catch (UnsatisfiedLinkError e) {
                        CefLog.Error("can't load jawt library: " + e.getMessage());
                    }
                    SystemBootstrap.loadLibrary("chrome_elf");
                    SystemBootstrap.loadLibrary("libcef");
                } else if (OS.isLinux()) {
                    SystemBootstrap.loadLibrary("cef");
                }

                SystemBootstrap.loadLibrary("jcef");
                CefApp.N_Startup(frameworkPath);
                ourStartupFeature.complete(null);
            } catch (Throwable e) {
                ourStartupFeature.completeExceptionally(e);
            }
        });
    }

    static class NamedThreadExecutor implements Executor {
        private final String name;

        NamedThreadExecutor(String name) {
            this.name = name;
        }

        @Override
        public void execute(Runnable command) {
            new Thread(command, name).start();
        }
    }

    private static native boolean N_Startup(String pathToCefFramework);

    private native boolean N_PreInitialize();

    private native boolean N_Initialize(CefAppHandler appHandler, CefSettings settings, boolean checkThread);

    private native void N_Shutdown();

    private native void N_DoMessageLoopWork();

    private native CefVersion N_GetVersion();

    private native boolean N_RegisterSchemeHandlerFactory(
            String schemeName, String domainName, CefSchemeHandlerFactory factory);

    private native boolean N_ClearSchemeHandlerFactories();
}
